package net.cloudcodex.client.widgets.spinner;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.LayoutPanel;

/**
 * Provides two arrows for in- and decreasing values. Add a
 * {@link SpinnerListener} to listen spinner change.
 */
public class Spinner {

	public interface SpinnerBundle extends ClientBundle {
		@Source("Spinner.css")
		SpinnerResource css();

		ImageResource arrowDown();

		ImageResource arrowDownDisabled();

		ImageResource arrowDownHover();

		ImageResource arrowDownPressed();

		ImageResource arrowUp();

		ImageResource arrowUpDisabled();

		ImageResource arrowUpHover();

		ImageResource arrowUpPressed();
	}

	public interface SpinnerResource extends CssResource {
		int ccSpinnerHeight();

		int ccSpinnerWidth();

		String ccDecrementArrow();

		String ccIncrementArrow();

		String normal();

		String disabled();

		String hover();

		String pressed();
	}

	public class Arrow {
		private final Image arrow = new Image();

		private final Command action;

		private final Timer timer = new Timer() {
			private int counter = 0;
			private int speed = initialSpeed;

			@Override
			public void cancel() {
				super.cancel();
				this.speed = initialSpeed;
				this.counter = 0;
			}

			@Override
			public void run() {
				this.counter++;
				if (this.speed <= 0 || this.counter % this.speed == 0) {
					this.speed--;
					this.counter = 0;
					action.execute();
				}
				if (this.speed < 0 && step < maxStep) {
					step += 1;
				}
			}
		};

		private final MouseDownHandler mouseDownHandler = new MouseDownHandler() {
			public void onMouseDown(MouseDownEvent event) {
				if (enabled) {
					arrow.addStyleName(bundle.css().pressed());
					action.execute();
					timer.scheduleRepeating(30);
				}
			}
		};

		private final MouseOverHandler mouseOverHandler = new MouseOverHandler() {
			public void onMouseOver(MouseOverEvent event) {
				if (enabled) {
					arrow.addStyleName(bundle.css().hover());
				}
			}
		};

		private final MouseOutHandler mouseOutHandler = new MouseOutHandler() {
			public void onMouseOut(MouseOutEvent event) {
				cancelTimer();
				arrow.removeStyleName(bundle.css().hover());
			}
		};

		private final MouseUpHandler mouseUpHandler = new MouseUpHandler() {
			public void onMouseUp(MouseUpEvent event) {
				cancelTimer();
			}
		};

		public Arrow(Command action, String styleName) {
			this.action = action;
			this.arrow.setStylePrimaryName(styleName);
			this.arrow.addStyleName(bundle.css().normal());
			this.arrow.addMouseDownHandler(this.mouseDownHandler);
			this.arrow.addMouseOverHandler(this.mouseOverHandler);
			this.arrow.addMouseOutHandler(this.mouseOutHandler);
			this.arrow.addMouseUpHandler(this.mouseUpHandler);
		}

		/**
		 * @return the image representing the arrow
		 */
		public Image getArrow() {
			return this.arrow;
		}

		/**
		 * Sets whether this arrow is enabled.
		 * 
		 * @param enabled
		 *            true to enable the arrow, false to disable it
		 */
		protected void setEnabled(boolean enabled) {
			arrow.setStyleName(bundle.css().disabled(), !enabled);
			if (!enabled) {
				cancelTimer();
			}
		}

		protected void cancelTimer() {
			step = minStep;
			this.arrow.removeStyleName(bundle.css().pressed());
			this.timer.cancel();
		}
	}

	/**
	 * Decreases the current value of the spinner by subtracting current step
	 */
	protected Command decrementAction = new Command() {
		@Override
		public void execute() {
			value -= step;
			if (constrained && value < min) {
				value = min;
				decrementArrow.cancelTimer();
			}
			fireOnValueChanged();
		}
	};

	/**
	 * Increases the current value of the spinner by adding current step
	 */
	protected Command incrementAction = new Command() {
		@Override
		public void execute() {
			value += step;
			if (constrained && value > max) {
				value = max;
				incrementArrow.cancelTimer();
			}
			fireOnValueChanged();
		}
	};

	private static final int INITIAL_SPEED = 7;

	protected static SpinnerBundle bundle = initSpinnerBundle();

	/**
	 * @return staticly initialized {@link SpinnerResource} as GWT recommends
	 */
	private static SpinnerBundle initSpinnerBundle() {
		final SpinnerBundle spinnerBundle = GWT.create(SpinnerBundle.class);
		spinnerBundle.css().ensureInjected();
		return spinnerBundle;
	}

	private final Arrow decrementArrow;
	private final Arrow incrementArrow;
	private final LayoutPanel panel;

	private List<SpinnerListener> spinnerListeners = new ArrayList<SpinnerListener>();
	private int step, minStep, maxStep, initialSpeed = INITIAL_SPEED;
	private long value, min, max;
	private boolean constrained;
	private boolean enabled = true;

	/**
	 * @param spinner
	 *            the widget listening to the arrows
	 * @param value
	 *            initial value
	 */
	public Spinner(SpinnerListener spinner, long value) {
		this(spinner, value, 0, 0, 1, 99, false);
	}

	/**
	 * @param spinner
	 *            the widget listening to the arrows
	 * @param value
	 *            initial value
	 * @param min
	 *            min value
	 * @param max
	 *            max value
	 */
	public Spinner(SpinnerListener spinner, long value, long min, long max) {
		this(spinner, value, min, max, 1, 99, true);
	}

	/**
	 * @param spinner
	 *            the widget listening to the arrows
	 * @param value
	 *            initial value
	 * @param min
	 *            min value
	 * @param max
	 *            max value
	 * @param minStep
	 *            min value for stepping
	 * @param maxStep
	 *            max value for stepping
	 */
	public Spinner(SpinnerListener spinner, long value, long min, long max, int minStep, int maxStep) {
		this(spinner, value, min, max, minStep, maxStep, true);
	}

	/**
	 * @param spinner
	 *            the widget listening to the arrows
	 * @param value
	 *            initial value
	 * @param min
	 *            min value
	 * @param max
	 *            max value
	 * @param minStep
	 *            min value for stepping
	 * @param maxStep
	 *            max value for stepping
	 * @param constrained
	 *            determines if min and max value will take effect
	 */
	public Spinner(SpinnerListener spinner, long value, long min, long max, int minStep, int maxStep,
			boolean constrained) {
		spinnerListeners.add(spinner);
		this.value = value;
		this.constrained = constrained;
		this.step = minStep;
		this.minStep = minStep;
		this.maxStep = maxStep;
		this.min = min;
		this.max = max;

		decrementArrow = new Arrow(decrementAction, bundle.css().ccDecrementArrow());
		incrementArrow = new Arrow(incrementAction, bundle.css().ccIncrementArrow());
		panel = new LayoutPanel();
		panel.add(incrementArrow.getArrow());
		panel.setWidgetTopHeight(incrementArrow.getArrow(), 0, Unit.PX, bundle.arrowUp().getHeight(), Unit.PX);
		panel.add(decrementArrow.getArrow());
		panel.setWidgetBottomHeight(decrementArrow.getArrow(), 0, Unit.PX, bundle.arrowDown().getHeight(), Unit.PX);
		panel.setPixelSize(bundle.css().ccSpinnerWidth(), bundle.css().ccSpinnerHeight());
		fireOnValueChanged();
	}

	public static SpinnerBundle getBundle() {
		return bundle;
	}

	/**
	 * @param listener
	 *            the listener to add
	 */
	public void addSpinnerListener(SpinnerListener listener) {
		spinnerListeners.add(listener);
	}

	/**
	 * @return the decreating arrow
	 */
	public Arrow getDecrementArrow() {
		return decrementArrow;
	}

	/**
	 * @return the increasing arrow
	 */
	public Arrow getIncrementArrow() {
		return incrementArrow;
	}

	/**
	 * @return the maximum value
	 */
	public long getMax() {
		return max;
	}

	/**
	 * @return the maximum spinner step
	 */
	public int getMaxStep() {
		return maxStep;
	}

	/**
	 * @return the minimum value
	 */
	public long getMin() {
		return min;
	}

	/**
	 * @return the minimum spinner step
	 */
	public int getMinStep() {
		return minStep;
	}

	public LayoutPanel getSpinnerPanel() {
		return this.panel;
	}

	/**
	 * @return the current value
	 */
	public long getValue() {
		return value;
	}

	/**
	 * @return true is min and max values are active, false if not
	 */
	public boolean isConstrained() {
		return constrained;
	}

	/**
	 * @return Gets whether this widget is enabled
	 */
	public boolean isEnabled() {
		return enabled;
	}

	/**
	 * @param listener
	 *            the listener to remove
	 */
	public void removeSpinnerListener(SpinnerListener listener) {
		spinnerListeners.remove(listener);
	}

	/**
	 * Sets whether this widget is enabled.
	 * 
	 * @param enabled
	 *            true to enable the widget, false to disable it
	 */
	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
		incrementArrow.setEnabled(enabled);
		decrementArrow.setEnabled(enabled);
	}

	/**
	 * @param initialSpeed
	 *            the initial speed of the spinner. Higher values mean lower
	 *            speed, default value is 7
	 */
	public void setInitialSpeed(int initialSpeed) {
		this.initialSpeed = initialSpeed;
	}

	/**
	 * @param max
	 *            the maximum value. Will not have any effect if constrained is
	 *            set to false
	 */
	public void setMax(long max) {
		this.max = max;
	}

	/**
	 * @param maxStep
	 *            the maximum step for this spinner
	 */
	public void setMaxStep(int maxStep) {
		this.maxStep = maxStep;
	}

	/**
	 * @param min
	 *            the minimum value. Will not have any effect if constrained is
	 *            set to false
	 */
	public void setMin(long min) {
		this.min = min;
	}

	/**
	 * @param minStep
	 *            the minimum step for this spinner
	 */
	public void setMinStep(int minStep) {
		this.minStep = minStep;
	}

	/**
	 * @param value
	 *            sets the current value of this spinner
	 * @param fireEvent
	 *            fires value changed event if set to true
	 */
	public void setValue(long value, boolean fireEvent) {
		this.value = value;
		if (fireEvent) {
			fireOnValueChanged();
		}
	}

	private void fireOnValueChanged() {
		for (SpinnerListener listener : spinnerListeners) {
			listener.onSpinning(value);
		}
	}

}
